แยกที่อยู่ภาษาไทยเป็นส่วนๆด้วย Javascript
Table of Contents
ข้อมูลที่อยู่หลายๆครั้งถูกเก็บเป็น string ไม่ว่าจะเป็นไฟล์ word หรืออยู่ใน chat log ต่างๆ ใช้ประโยชน์ต่อยากเพราะไม่สามารถ query อะไรได้แบบ database เลย วันนี้ผมเลยอยากเสนอหนทางในการแยกมันออกเป็นส่วนๆเพื่อนำไปใช้ในต่อกันครับ
ปัญหา #
ผมมีร้านขายเครื่องจักรกลการเกษตรอยู่ มีขายทั้งหน้าร้านแบบ offline และ online ซึ่งก็ดูจะปกติดีใครๆก็ทำกัน แต่ปัญหาก็เกิดเมื่อจากสถิติแล้วลูกค้าของผมส่วนมากจะค่อนข้าง low-technology การจะบอกให้เขาเข้า web มากรอก form เลือกสินค้าแล้วกรอกที่อยู่จัดส่งเองเป็นเรื่องที่ยากมาก แค่ลูกค้ามี LINE หรือ Facebook Messenger ก็ถือว่าดีมากแล้ว
ทีนี้เวลาเขาจะสั่งซื้อเขาก็ต้องพิมพ์ทุกอย่างมาใน chat ใช่ไหมครับ แปลว่าข้อมูลที่อยู่อันมีค่าเหล่านี้จะไม่ได้ถูกจัด format เหมือนการกรอก form เข้ามา การที่เราจะเอาข้อมูลลูกค้าไปเก็บลง database ก็เลยต้องเป็นระบบมือซะ 100% ตรงนี้ทำให้เสียเวลาพอสมควร ผมเลยต้องมองหาวิธีที่จะกรอกข้อมูลลูกค้าให้เร็วที่สุดโดยที่ไม่ต้อง copy & paste แบบเดิมๆ
วิธีแก้ปัญหา #
หลังจากได้ string ที่อยู่มา ผมก็จะเขียน script ขึ้นมาเพื่อแยก string ชุดนี้ออกเป็นส่วนๆ โดยพึ่งพา Regular Expression แบบโง่ๆ แต่เพื่อให้เกิดความผิดพลาดน้อยที่สุด เราต้องมีข้อมูลที่อยู่หลักๆอย่าง ตำบล/แขวง อำเภอ/เขต จังหวัด และรหัสไปรษณีย์ซะก่อน
หารายชื่อ ตำบล อำเภอ จังหวัด รหัสไปรษณีย์ #
ด้วยความที่เป็นพ่อค้าออนไลน์อยู่ก็ได้ใช้บริการเว็บ EasyShip ของ Kerry Express เป็นประจำผมก็ไปสะดุดตากับตัว auto-complete ที่มีขึ้นมาให้ตลอด ผมเลยลองไป inspect ดูก็พบว่า “อ้าว!! ส่งมาทั้งประเทศเลยนี่หน่า” ดังนั้นเรื่องไฟล์ที่อยู่ก็จบไปครับ เราได้ “ตำบล อำเภอ จังหวัด รหัสไปรษณีย์” ของทั้งประเทศมาแล้ว แถมเชื่อถือได้ด้วยเพราะถูกเอามาใช้จริง รออะไรล่ะครับดาวน์โหลด json file นี้มาเลยสิ

หาว่าที่อยู่นี้อยู่ใน “ตำบล อำเภอ จังหวัด รหัสไปรษณีย์” ไหน #
เพื่อให้รู้ว่าอยู่ที่ไหนผมจะทำแบบโง่ๆเลย คือการซอย string นี้ออกมาเป็นคำๆแล้วโยนเข้าไป search ใน list ของทั้งประเทศครับ พอวนครบทุกคำแล้ว เราจะมาดูว่าตำบลไหนตรงกับคำที่เราซอยออกไปมากที่สุด ตำบลนั้นก็น่าจะใช่แล้วล่ะครับ
- เริ่มจากสร้างไฟล์
index.js
เลยครับ โดยมี functionsplit()
เป็นตัวหลักของวันนี้ครับ แล้วเราก็จะรับ text จาก command line ด้วย ซึ่งตอนแรกที่ run ใน log ต้องได้address:
ก็จะยังไม่มีอะไร เพราะเรายังไม่ได้ทำอะไร
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const split = async (text) => {
try {
console.log('input text:', text);
const result = '';
console.log('address :', result)
} catch (error) {
console.error(error);
}
};
const arguments = process.argv;
if (arguments.length > 2) {
const input = arguments.slice(2)[0];
split(input);
} else {
console.log('no input');
}
- เราจะลบคำนำหน้าอย่าง
ตำบล | แขวง | อำเภอ | เขต | จังหวัด
ออกไปก่อนครับ เวลา search มันจะได้ไม่เอาคำนำหน้าไปด้วย อย่าลืมนะครับว่าผู้ใช้จำพิมพ์มายังไงก็ได้ เช่น “อำเภอ” เขาอาจจะพิมพ์แค่ “อ.” ก็ได้ เดี๋ยวเวลาไปหาใน list มันจะไม่เจอ ผมสร้าง function ชื่อremovePrefix(text)
ขึ้นมาเพื่อการนี้โดยเฉพาะ
const removePrefix = (text) => {
const prefixPattern = /(เขต|แขวง|จังหวัด|อำเภอ|ตำบล|อ\.|ต\.|จ\.)/g;
let string = text.replace(/\s+/g, ' '); //มี space เยอะก็ลดเหลือ 1 พอ
string = string.replace(prefixPattern, '');
return string;
}
แล้วก็เรียกใช้ใน split()
ซะ พร้อมกับสร้าง wordlist ด้วยการหั่น string ออกด้วยช่องว่าง แล้วยัง filter เอาเฉพาะคำที่มีความยาวมากกว่าหรือเท่ากับ 3 อักขระขึ้นไป เพราะคงไม่มีชื่อตำบลหรืออำเภอที่สั้นกว่า 3 ตัวแล้วมั้ง จะได้ลดจำนวนรอบในการวนหาต่อไป
const split = async (text) => {
try {
console.log('input text:', text);
const cleanText = removePrefix(text);
const wordlist = cleanText.split(' ').filter(word => word.length >= 3);
const result = '';
console.log('address :', result)
} catch (error) {
console.error(error);
}
};
- เอา wordlist ที่ได้ไปวนหาในรายชื่อตำบลทั้งประเทศ ใน
findSubdistrcit(wordlist)
โดยอ่านมาจาก json file ที่โหลดมาตอนแรกนั่นแหละครับ
const findSubdistrict = async (wordlist) => {
const content = await readFile('subdistricts.json', 'utf-8');
const subdistricts = JSON.parse(content);
let results = [];
//วนหาแล้วต่อ result ให้ยาวไปเรื่อยๆ ซ้ำก็ไม่เป็นไรเดี๋ยวไปนับทีหลัง
for (let word of wordlist) {
const filtered = subdistricts.filter(item => {
return item.name.includes(word)
});
results = results.concat(filtered);
}
//เปลี่ยน format ให่้เหลือแต่ string ใน array อย่างเดียว
const matches = results.map(item => item.name);
//หาตัวที่ซ้ำบ่อยที่สุด
const bestMatched = findBestMatched(matches).name.split(', ');
return {
subdistrict: bestMatched[0],
district: removePrefix(bestMatched[1]),
province: removePrefix(bestMatched[2]),
zipcode: bestMatched[3]
};
};
const findBestMatched = (matches) => {
let group = {};
//จับเข้ากลุ่่มกันพร้อมกับนับด้วย
matches.forEach((i) => {
group[i] = (group[i] || 0) + 1;
});
//เปลี่ยนเป็น array เพราะจะได้ sort ง่ายๆ
let results = Object.keys(group).map(key => {
return {
name: key,
count: group[key]
}
});
//ส่งเอาเฉพาะตัวที่ซ้ำเยอะที่สุดกลับไป
return results.sort((a, b) => {
if (a.count > b.count) {
return -1;
}
if (a.count > b.count) {
return 1;
}
return 0;
})[0];
}
- ได้ที่อยู่หลัก (ตำบล, อำเภอ, จังหวัด, รหัสไปรษณีย์) แล้วนะ
const split = async (text) => {
try {
console.log('input text:', text);
const cleanText = removePrefix(text);
const wordlist = cleanText.split(' ').filter(word => word.length >= 3);
//ได้มาแล้ว
const mainAddress = await findSubdistrict(wordlist);
console.log('address :', result)
} catch (error) {
console.error(error);
}
};
หาชื่อ เบอร์โทร และที่อยู่ย่อย ที่เหลือ #
หลังจากได้ ตำบล, อำเภอ, จังหวัด, รหัสไปรษณีย์ งานของเรายังไม่จบแค่นั้นครับ เรายังเหลืออีกหลายส่วน แถมยากด้วย แต่ผมก็จะใช้วิธีโง่ๆด้วยการตัดตำที่มั่นใจออกไปเรื่อยๆครับ จนสุดท้ายเราจะได้ก้อนที่อยู่ย่อยกระจุกกันอยู่ เราก็จะเอาไอ้ตัวนั้นแหละมาปิดงานของเรา โดยทั้งหมดนี้ผมจะทำใน finalResult(text, mainAddress)
นะครับ
const finalResult = (text, mainAddress) => {
const namePattern = /(เด็กชาย|เด็กหญิง|ด\.ช\.|ด\.ญ\.|นาย|นาง|นางสาว|น\.ส\.|ดร\.)([ก-๙]+\s[ก-๙]+)/;
const phonePattern = /(08\d{1}-\d{3}-\d{4}|08\d{1}-\d{7}|08\d{8})/;
let remainingTxt = text;
//ตัดชื่อ ตำบล แขวง เขต จังหวัด รหัสไปรษณีย์ ที่เราได้มาแล้วออกไปก่อน
const keyPattern = Object.values(mainAddress);
keyPattern.forEach(key => {
remainingTxt = remainingTxt.replace(key, '').trim();
});
//หาชื่อจาก pattern ที่มีคำนำหน้าและภาษาไทย 2 ก้อน แล้วก็เก็บลงไปในตัวแปร
const nameMatched = remainingTxt.match(namePattern);
let name = '';
if (nameMatched) {
[name] = nameMatched
}
//เสร็จแล้วก็ลบออกจาก text ด้วย
remainingTxt = remainingTxt.replace(name, '').trim();
//หาเบอร์โทร อันนี้น่่าจะง่ายกว่าหาชื่อครับตัวเลขล้วนๆขึ้นต้นด้วย 08
const phoneMatched = remainingTxt.match(phonePattern);
let phone = '';
if (phoneMatched) {
[phone] = phoneMatched
}
//อย่าลืมลบออกเหมือนกันนะ
remainingTxt = remainingTxt.replace(phone, '').trim();
//เอาพวก "-" ออกไปเพราะบางคนอาจจะใส่หรือไม่ใส่เราก็ไม่รู้ เอาออกเลยดีกว่า
phone = phone.replace(/-/g, '');
//บางครั้งคนเราจะชอบใส่เบอร์ในวงเล็บ (081-222-3333) มันจะเหลือแต่ () เราก็ลบทิ้งไป
remainingTxt = remainingTxt.replace('()', '').trim();
//ก้อนสุดท้ายขอทึกทักเอาเองเลยว่ามันคือ ที่อยู่ย่อยๆ ซึ่งผมจะไม่เอาไปแยกนะครับ ผมพอใจแล้ว
const address = remainingTxt.replace(/\s+/g, ' ').trim();
return {
name,
phone,
address,
...mainAddress
}
}
ลองจับใส่ split()
แล้ว run
const split = async (text) => {
try {
console.log('input text:', text);
const cleanText = removePrefix(text);
const wordlist = cleanText.split(' ').filter(word => word.length >= 3);
const mainAddress = await findSubdistrict(wordlist);
const result = finalResult(cleanText, mainAddress);
console.log('address :', result)
} catch (error) {
console.error(error);
}
};
ลอง run ด้วยคำสั่งบน terminal
node index "นายดราก้อน ตันเด้อ อาคารเอ ชั้น 10 (081-234-5678) ห้อง 3 เขตพญาไท กรุงเทพมหานคร แขวงสามเสนใน 10400"

ตัวอย่าง code #
เรียบร้อยครับ กับการแยกที่อยู่คนไทย ภาษาไทยแบบคร่าวๆ ถึงแม้มันจะยังไม่สามารถทำงานได้ 100% อีกอย่างถ้าเอาไปใช้บน web จริงๆก็ควรจะลดขนาด subdistrict.json ลงไปด้วย แต่ตอนนี้มันก็ช่วยลดเวลาการ copy paste ไปได้มากพอสมควรเลยครับ สำหรับท่านไหนที่มีข้อเสนอแนะสามารถบอกผมได้เลยครับ ผมยินดีมากที่จะได้ปรับปรุงครับ สำหรับท่านใดที่มีคำถามสามารถสอบถามเข้ามาที่ Inbox ของ Facebook Page ได้เลยนะครับ